Skip to content

fix: address review comments from PRs #2604 and #2619#2634

Merged
rmusser01 merged 2 commits into
devfrom
fix/pr2604-2619-review-followup
Jul 4, 2026
Merged

fix: address review comments from PRs #2604 and #2619#2634
rmusser01 merged 2 commits into
devfrom
fix/pr2604-2619-review-followup

Conversation

@rmusser01

@rmusser01 rmusser01 commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up addressing every unresolved reviewer comment from the two audit-fix PRs merged earlier today (the client-reuse and trivial-test comments were already fixed in-branch before merge; the nonlocal suggestion on #2619 was correctly rejected by the author).

From PR #2604 (egress hardening)

  • Blocking I/O in async (qodo, search.py:207): run_arxiv_download_adapter now offloads Path.write_bytes via asyncio.to_thread. (The import arxiv branch's blocking library calls are pre-existing and out of scope.)
  • Redirect leaks auth headers (qodo, tokenizer_resolver.py:664, security): _http_post now sends allow_redirects=False — these POSTs carry Authorization/x-api-key and the central redirect loop doesn't strip headers cross-origin. Regression test contract updated. The central fix (strip/partition sensitive headers on cross-origin redirects in http_client fetch/afetch) is qodo's preferred Option B and remains open — larger change, deserves its own PR.
  • Silent suppress in finally (qodo, tokenizer_resolver.py:895): restoring encoding.timeout_seconds now logs a warning on failure instead of suppress(Exception).
  • Wrong task id (qodo, plan doc): TASK-12138TASK-12146.

From PR #2619 (ACP reconnect cleanup)

  • Workflow logic in endpoint (qodo, architecture): reconnect-replay lifecycle extracted to core — ws_broadcaster.start_reconnect_replay() / stop_reconnect_replay(); the endpoint just calls them.
  • Cleanup suppresses exceptions (qodo, agent_client_protocol.py:1343): teardown failures are now logged in stop_reconnect_replay, never silently dropped.
  • Missing docstring (qodo): WSBroadcaster.__init__ documents consumer_id.
  • Disallowed asyncio marker + missing type hints on new tests (qodo): dropped redundant @pytest.mark.asyncio from the new tests (asyncio_mode=auto), added module-level pytestmark = pytest.mark.unit to test_ws_broadcaster.py, completed type hints on the new test helpers.

Bonus

  • test_determine_permission_tier_batch was red on dev: it pinned pre-hardening behavior (write → batch). Updated for the hardened contract (write/modify → individual; true fallback tools → batch).

Verification

  • tests/Agent_Client_Protocol/: 963/963 pass locally
  • test_tokenizer_resolver_unit.py + test_research_adapters.py: 117/117 pass
  • Local runs use PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 + -p pytest_asyncio.plugin per repo convention

🤖 Generated with Claude Code


Summary by cubic

Follow-up to #2604 (egress hardening) and #2619 (ACP reconnect) that closes remaining review comments. Prevents credential leaks on redirects, removes event-loop blocking I/O, and hardens reconnect replay with lifecycle helpers, error cleanup, and clearer logs/tests. Updates plan doc to reference TASK-12146 for audit verification.

  • Bug Fixes

    • Tokenizer POST: allow_redirects=False to avoid sending auth headers on redirects; tests updated.
    • ArXiv download: offload Path.write_bytes via asyncio.to_thread to avoid event-loop stalls.
    • Tokenizer timeout restore now logs failures instead of suppressing errors.
    • Reconnect replay: start_reconnect_replay now stops the broadcaster if add_connection fails mid-replay to prevent leaked bus subscriptions; regression test added.
    • Permission tier tests updated to hardened policy: write/modify → individual; true fallbacks → batch.
    • Plan doc traceability: TASK-12146.
  • Refactors

    • Extracted reconnect replay lifecycle to core: start_reconnect_replay / stop_reconnect_replay; endpoint now only calls them.
    • Cleanup paths now log teardown failures with bound action/conn_id/consumer_id (no silent suppress).
    • WSBroadcaster.__init__ documents consumer_id; tests drop redundant @pytest.mark.asyncio, add unit marker, and complete type hints.

Written for commit dd8660b. Summary will update on new commits.

Review in cubic

PR #2604 (egress hardening) follow-ups:
- run_arxiv_download_adapter: offload blocking Path.write_bytes to a
  thread (event-loop stall under load)
- tokenizer_resolver._http_post: allow_redirects=False — these POSTs
  carry provider credentials; following a cross-origin redirect would
  resend secrets to the redirect target (test contract updated)
- tokenizer_resolver: log (not suppress) failures restoring
  encoding.timeout_seconds in the runtime-probe finally block
- egress remediation plan: TASK-12138 -> TASK-12146 traceability fix

PR #2619 (ACP reconnect cleanup) follow-ups:
- extract reconnect-replay lifecycle into core:
  ws_broadcaster.start_reconnect_replay / stop_reconnect_replay
  (endpoint no longer owns broadcaster workflow logic)
- cleanup failures are now logged instead of contextlib.suppress'ed
- WSBroadcaster.__init__: document the consumer_id parameter
- new tests: drop redundant @pytest.mark.asyncio (asyncio_mode=auto),
  add module-level unit marker to test_ws_broadcaster.py, complete
  type hints on new test helpers
- test_determine_permission_tier_batch: was red on dev — pinned
  pre-hardening behavior (write->batch); updated for the hardened
  contract (write/modify->individual, fallback->batch)

Verified: 963/963 tests in tests/Agent_Client_Protocol/, 117/117 in
tokenizer/research-adapter files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2af69b87-e4ee-4f53-823e-3bdecf6fba01

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/pr2604-2619-review-followup

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Follow up audit-fix reviews: safer redirects, async file I/O, ACP replay lifecycle helpers

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Prevent credential leakage by disabling redirects for tokenizer provider POST requests.
• Avoid event-loop stalls by offloading arXiv PDF writes to a worker thread.
• Move ACP reconnect-replay lifecycle into core helpers and harden cleanup/logging.
Diagram

graph TD
  client["WebSocket client"] --> acp["ACP websocket endpoint"] --> ws["ws_broadcaster helpers"] --> bus["SessionEventBus"]
  tok["tokenizer_resolver"] --> http["http_client.fetch"]
  research["research/search adapter"] --> io["Threaded file write"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Central redirect sanitization in http_client
  • ➕ Prevents auth header leakage for all callers, not just tokenizer_resolver
  • ➕ Keeps redirect behavior available while stripping/partitioning sensitive headers cross-origin
  • ➖ Broader behavioral change; higher regression risk across all HTTP call sites
  • ➖ Requires careful rules for same-origin vs cross-origin and header allowlists
2. Async file writer (aiofiles) instead of asyncio.to_thread
  • ➕ Cleaner async semantics and potentially better scalability under heavy I/O
  • ➕ Makes file writes explicit/consistent across adapters
  • ➖ Adds/relies on an extra dependency and requires refactoring call sites
  • ➖ Still doesn’t help with other blocking library calls (e.g., arxiv client)
3. Reconnect-replay as an async context manager
  • ➕ Enforces pairing of start/stop and reduces endpoint boilerplate
  • ➕ Makes cleanup semantics harder to misuse in future endpoints
  • ➖ More API surface change and potential churn for existing call patterns
  • ➖ May be overkill for the current single primary usage

Recommendation: Keep this PR’s targeted fixes: disabling redirects for credentialed tokenizer POSTs and moving reconnect-replay lifecycle into ws_broadcaster helpers are low-churn, high-signal changes that address concrete review/security concerns. Consider a follow-up PR for the broader http_client redirect/header-sanitization approach if redirect support is needed across the codebase.

Files changed (8) +119 / -29

Bug fix (3) +27 / -14
agent_client_protocol.pyDelegate reconnect replay lifecycle to ws_broadcaster core helpers +12/-11

Delegate reconnect replay lifecycle to ws_broadcaster core helpers

• Replaces in-endpoint reconnect broadcaster wiring with start_reconnect_replay/stop_reconnect_replay helpers. Cleanup now relies on core logging behavior rather than suppressing teardown exceptions in the endpoint.

tldw_Server_API/app/api/v1/endpoints/agent_client_protocol.py

tokenizer_resolver.pyDisable redirects for credentialed tokenizer POSTs and log restore failures +12/-2

Disable redirects for credentialed tokenizer POSTs and log restore failures

• Sets allow_redirects=False on provider tokenizer POST requests to avoid credential leakage on redirects. Replaces silent exception suppression when restoring encoding.timeout_seconds with a warning log.

tldw_Server_API/app/core/LLM_Calls/tokenizer_resolver.py

search.pyOffload blocking PDF writes in async arXiv download adapter +3/-1

Offload blocking PDF writes in async arXiv download adapter

• Uses asyncio.to_thread to run Path.write_bytes without blocking the event loop when downloading PDFs via managed async fetch.

tldw_Server_API/app/core/Workflows/adapters/research/search.py

Refactor (1) +58 / -0
ws_broadcaster.pyAdd reconnect-replay start/stop helpers and document consumer_id usage +58/-0

Add reconnect-replay start/stop helpers and document consumer_id usage

• Adds a docstring clarifying why unique consumer_id values matter when subscribing to the event bus. Introduces start_reconnect_replay/stop_reconnect_replay helpers and logs (rather than suppresses) failures during disconnect cleanup.

tldw_Server_API/app/core/Agent_Client_Protocol/consumers/ws_broadcaster.py

Tests (3) +33 / -14
test_acp_websocket.pyTighten ACP reconnect replay cleanup test typing and update permission tier expectations +27/-11

Tighten ACP reconnect replay cleanup test typing and update permission tier expectations

• Removes redundant pytest asyncio marker (repo uses asyncio_mode=auto) and adds missing type hints to test helpers. Updates batch/individual permission tier assertions to match the hardened contract (write/modify -> individual; fallback tools -> batch).

tldw_Server_API/tests/Agent_Client_Protocol/test_acp_websocket.py

test_ws_broadcaster.pyMark ws_broadcaster tests as unit and remove redundant asyncio marker +3/-2

Mark ws_broadcaster tests as unit and remove redundant asyncio marker

• Adds module-level pytestmark=pytest.mark.unit and updates a test signature/marker usage to align with asyncio_mode=auto conventions.

tldw_Server_API/tests/Agent_Client_Protocol/test_ws_broadcaster.py

test_tokenizer_resolver_unit.pyUpdate tokenizer POST contract test for redirects disabled +3/-1

Update tokenizer POST contract test for redirects disabled

• Adjusts the mocked fetch call expectation to require allow_redirects=False for tokenizer_resolver._http_post.

tldw_Server_API/tests/Writing/test_tokenizer_resolver_unit.py

Documentation (1) +1 / -1
2026-07-04-audit-integrations-egress-remediation-plan.mdFix audit plan task ID for verification record +1/-1

Fix audit plan task ID for verification record

• Updates Stage 4 success criteria to reference the correct tracking task ID (TASK-12146).

Docs/superpowers/plans/2026-07-04-audit-integrations-egress-remediation-plan.md

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several improvements and security hardening measures, including refactoring the WebSocket reconnect-replay logic to use helper functions with explicit error logging, disabling redirects on tokenizer POST requests to prevent credential leakage, and offloading blocking file writes to a separate thread using asyncio.to_thread. Additionally, test suites have been updated to align with permission tier hardening. Feedback on these changes identifies a potential resource leak in start_reconnect_replay if add_connection fails after the broadcaster has started, and suggests wrapping the call in a try...except block to ensure the broadcaster is stopped on failure.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +209 to +231
async def start_reconnect_replay(
bus: SessionEventBus,
*,
conn_id: str,
send_callback: SendCallback,
from_sequence: int,
) -> WSBroadcaster:
"""Create and start a dedicated broadcaster that replays missed events.

Used on WebSocket reconnect: a per-connection broadcaster (unique
``consumer_id`` so concurrent reconnects don't clobber each other's bus
subscriptions) is subscribed to *bus* and replays buffered events from
*from_sequence* to *send_callback*. Callers must pair this with
:func:`stop_reconnect_replay` on disconnect.
"""
broadcaster = WSBroadcaster(consumer_id=f"ws_broadcaster:{conn_id}")
await broadcaster.start(bus)
await broadcaster.add_connection(
conn_id=conn_id,
send_callback=send_callback,
from_sequence=from_sequence,
)
return broadcaster

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If add_connection raises an exception after broadcaster.start(bus) has succeeded, the broadcaster will remain active and subscribed to the event bus, leading to a resource/memory leak. Since the caller (agent_client_protocol.py) only receives the broadcaster reference upon successful return, it won't be able to clean it up in its finally block. Wrapping add_connection in a try...except block to stop the broadcaster on failure ensures proper cleanup.

async def start_reconnect_replay(
    bus: SessionEventBus,
    *,
    conn_id: str,
    send_callback: SendCallback,
    from_sequence: int,
) -> WSBroadcaster:
    """Create and start a dedicated broadcaster that replays missed events.

    Used on WebSocket reconnect: a per-connection broadcaster (unique
    ``consumer_id`` so concurrent reconnects don't clobber each other's bus
    subscriptions) is subscribed to *bus* and replays buffered events from
    *from_sequence* to *send_callback*. Callers must pair this with
    :func:`stop_reconnect_replay` on disconnect.
    """
    broadcaster = WSBroadcaster(consumer_id=f"ws_broadcaster:{conn_id}")
    await broadcaster.start(bus)
    try:
        await broadcaster.add_connection(
            conn_id=conn_id,
            send_callback=send_callback,
            from_sequence=from_sequence,
        )
    except Exception:
        await broadcaster.stop()
        raise
    return broadcaster

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in dd8660b: add_connection is now wrapped so a mid-replay failure stops the broadcaster (best-effort, logged) before re-raising; regression test test_start_reconnect_replay_cleans_up_when_registration_fails asserts no leaked bus subscriber.

@qodo-code-review

qodo-code-review Bot commented Jul 4, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 74 rules

Grey Divider


Action required

1. start_reconnect_replay() docstring incomplete ✓ Resolved 📘 Rule violation ✧ Quality
Description
The new start_reconnect_replay() docstring lacks explicit Args: and Returns: sections, so
parameter meanings and the returned WSBroadcaster are not documented per the comprehensive
docstring requirement. This reduces maintainability and makes the public contract unclear.
Code

tldw_Server_API/app/core/Agent_Client_Protocol/consumers/ws_broadcaster.py[R216-223]

+    """Create and start a dedicated broadcaster that replays missed events.
+
+    Used on WebSocket reconnect: a per-connection broadcaster (unique
+    ``consumer_id`` so concurrent reconnects don't clobber each other's bus
+    subscriptions) is subscribed to *bus* and replays buffered events from
+    *from_sequence* to *send_callback*. Callers must pair this with
+    :func:`stop_reconnect_replay` on disconnect.
+    """
Evidence
Rule 380617 requires function docstrings to document parameters and return values. The added
docstring for start_reconnect_replay() provides narrative text but does not include explicit
Args:/Returns: sections for bus, conn_id, send_callback, from_sequence, or the returned
WSBroadcaster.

Rule 380617: Require comprehensive docstrings for modules, classes, and functions
tldw_Server_API/app/core/Agent_Client_Protocol/consumers/ws_broadcaster.py[209-231]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`start_reconnect_replay()` is a newly added public function but its docstring does not include the required structured documentation (at minimum `Args:` and `Returns:`).

## Issue Context
Compliance requires comprehensive docstrings for modified/new functions, including parameter documentation and return value description.

## Fix Focus Areas
- tldw_Server_API/app/core/Agent_Client_Protocol/consumers/ws_broadcaster.py[209-231]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Reconnect replay cleanup leak ✓ Resolved 🐞 Bug ☼ Reliability
Description
start_reconnect_replay() subscribes/spawns a WSBroadcaster task before replaying buffered events; if
replay (send_callback) raises, the broadcaster is never returned so the endpoint finally-block won’t
call stop_reconnect_replay(), leaving a live bus subscriber/task behind. This can accumulate under
flaky reconnects and cause memory/CPU growth over time.
Code

tldw_Server_API/app/core/Agent_Client_Protocol/consumers/ws_broadcaster.py[R224-231]

+    broadcaster = WSBroadcaster(consumer_id=f"ws_broadcaster:{conn_id}")
+    await broadcaster.start(bus)
+    await broadcaster.add_connection(
+        conn_id=conn_id,
+        send_callback=send_callback,
+        from_sequence=from_sequence,
+    )
+    return broadcaster
Evidence
The endpoint only calls stop_reconnect_replay() when reconnect_broadcaster is assigned; if
start_reconnect_replay() raises after subscribing, reconnect_broadcaster stays None and cleanup is
skipped. WSBroadcaster.start() subscribes/spawns a task, and add_connection() can raise during
replay because it awaits send_callback, so the failure window is real and results in a leaked
subscriber/task without an explicit stop().

tldw_Server_API/app/api/v1/endpoints/agent_client_protocol.py[1277-1292]
tldw_Server_API/app/api/v1/endpoints/agent_client_protocol.py[1337-1345]
tldw_Server_API/app/core/Agent_Client_Protocol/consumers/ws_broadcaster.py[149-170]
tldw_Server_API/app/core/Agent_Client_Protocol/consumers/ws_broadcaster.py[81-105]
tldw_Server_API/app/core/Agent_Client_Protocol/consumers/ws_broadcaster.py[209-231]
tldw_Server_API/app/core/Agent_Client_Protocol/consumers/replay_utils.py[16-43]
tldw_Server_API/app/core/Agent_Client_Protocol/event_bus.py[67-108]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`start_reconnect_replay()` creates a `WSBroadcaster`, calls `start()` (subscribes + creates a consume-loop task), and then calls `add_connection()` (which may replay buffered events and invoke the provided `send_callback`). If `add_connection()` (or its replay/send) raises, the helper raises before returning the broadcaster to the caller.

In `agent_client_protocol.py`, cleanup only runs when `reconnect_broadcaster` is non-`None`. Because the assignment never completes on exception, cleanup is skipped, leaking the bus subscriber entry and the background task.

### Issue Context
`WSBroadcaster.start()` subscribes to `SessionEventBus` and spawns a task; unsubscribing only happens in `WSBroadcaster.stop()`. `add_connection()` can raise because it replays buffered events by awaiting `send_callback`.

### Fix Focus Areas
- tldw_Server_API/app/core/Agent_Client_Protocol/consumers/ws_broadcaster.py[209-231]

### Recommended fix
Wrap the body of `start_reconnect_replay()` in a `try/except` so that if anything after `broadcaster.start(bus)` fails, you `await broadcaster.stop()` (best-effort) before re-raising.

Example pattern:
```py
broadcaster = WSBroadcaster(...)
try:
   await broadcaster.start(bus)
   await broadcaster.add_connection(...)
   return broadcaster
except Exception:
   with suppress(Exception):
       await broadcaster.stop()
   raise
```

(Alternative: return the broadcaster immediately after `start()` and let the caller perform `add_connection()` + cleanup, but the above keeps the new abstraction.)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. stop_reconnect_replay() logs lack context ✓ Resolved 📘 Rule violation ◔ Observability
Description
The new warning logs in stop_reconnect_replay() do not include structured contextual fields (e.g.,
action, conn_id, consumer_id) as keyword arguments/bound context. This makes error logs harder
to correlate and search, especially during disconnect/teardown failures.
Code

tldw_Server_API/app/core/Agent_Client_Protocol/consumers/ws_broadcaster.py[R245-255]

+            logger.warning(
+                "Failed to remove reconnect replay connection {}: {}", conn_id, exc
+            )
+    try:
+        await broadcaster.stop()
+    except Exception as exc:
+        logger.warning(
+            "Failed to stop reconnect replay broadcaster {}: {}",
+            broadcaster.consumer_id,
+            exc,
+        )
Evidence
Rule 380623 requires contextual data in error-condition logs using loguru. The added
logger.warning(...) calls log only positional-formatted strings, without binding or passing
structured fields like action, conn_id, or consumer_id as keyword arguments.

Rule 380623: Include contextual data in error logs with loguru
tldw_Server_API/app/core/Agent_Client_Protocol/consumers/ws_broadcaster.py[241-255]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`stop_reconnect_replay()` emits `logger.warning(...)` messages without structured context fields (keyword args or `logger.bind(...)`).

## Issue Context
Compliance requires contextual identifiers/operation info in error-condition logs using loguru, ideally as structured fields for correlation and filtering.

## Fix Focus Areas
- tldw_Server_API/app/core/Agent_Client_Protocol/consumers/ws_broadcaster.py[241-255]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread tldw_Server_API/app/core/Agent_Client_Protocol/consumers/ws_broadcaster.py Outdated
… cleanup logs

Addresses PR #2634 review comments:
- start_reconnect_replay: if add_connection raises mid-replay, stop the
  broadcaster (best-effort, logged) before re-raising — otherwise the
  bus subscription and consume task leak because the caller never
  receives the reference to clean up. Regression test added.
- stop_reconnect_replay + failure path: logger.bind(action, conn_id,
  consumer_id) for correlatable teardown warnings.
- start_reconnect_replay docstring: full Args/Returns/Raises sections.

Verified: 964/964 tests in tests/Agent_Client_Protocol/.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rmusser01 rmusser01 merged commit f69c130 into dev Jul 4, 2026
18 of 19 checks passed
rmusser01 added a commit that referenced this pull request Jul 4, 2026
- Task 4 nightly shuffle: use -p pytest_randomly (full module name),
  not the entry-point alias, per review — more reliable under
  PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 (gemini review)
- Add tracked follow-up: central http_client cross-origin redirect
  header stripping (qodo Option B from PR #2604), superseding the
  per-caller allow_redirects=False mitigation from PR #2634
- fake-indexeddb package.json comment was already addressed in a
  prior revision

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant